<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>豆瓣书籍数据可视化</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        /* 全局样式 */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
        }
        
        body {
            background-color: #f5f5f5;
            color: #333;
            line-height: 1.6;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }
        
        header {
            background: linear-gradient(135deg, #228B22, #32CD32);
            color: white;
            padding: 40px 0;
            text-align: center;
            margin-bottom: 30px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
        }
        
        h1 {
            font-size: 2.5rem;
            margin-bottom: 10px;
        }
        
        h2 {
            font-size: 1.8rem;
            margin: 30px 0 20px;
            color: #228B22;
            border-bottom: 2px solid #e0e0e0;
            padding-bottom: 10px;
        }
        
        /* 书籍列表样式 */
        .books-container {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
            margin-bottom: 40px;
        }
        
        .books-scroll {
            height: 600px;
            overflow-y: auto;
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }
        
        .book-card {
            background-color: white;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            transition: transform 0.3s ease, box-shadow 0.3s ease;
            display: flex;
            flex-direction: column;
            height: 100%;
        }
        
        .book-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
        }
        
        .book-cover {
            height: 200px;
            overflow: hidden;
        }
        
        .book-cover img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            transition: transform 0.5s ease;
        }
        
        .book-card:hover .book-cover img {
            transform: scale(1.05);
        }
        
        .book-info {
            padding: 15px;
            flex-grow: 1;
            display: flex;
            flex-direction: column;
        }
        
        .book-title {
            font-size: 1rem;
            font-weight: bold;
            margin-bottom: 5px;
            color: #333;
        }
        
        .book-author {
            font-size: 0.9rem;
            color: #666;
            margin-bottom: 5px;
        }
        
        .book-rating {
            display: flex;
            align-items: center;
            margin-bottom: 5px;
        }
        
        .rating-stars {
            color: #ff9800;
            margin-right: 5px;
        }
        
        .rating-value {
            font-weight: bold;
            color: #333;
        }
        
        .book-categories {
            display: flex;
            flex-wrap: wrap;
            gap: 5px;
            margin-top: auto;
        }
        
        .category-tag {
            background-color: #e0f2e9;
            color: #228B22;
            font-size: 0.7rem;
            padding: 3px 8px;
            border-radius: 12px;
        }
        
        /* 可视化图表样式 */
        .chart-container {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            margin-bottom: 40px;
            position: relative;
        }
        
        .chart-title {
            text-align: center;
            margin-bottom: 20px;
            font-weight: bold;
            color: #228B22;
        }
        
        svg {
            width: 100%;
            height: 400px;
            overflow: visible;
        }
        
        .bubble {
            cursor: pointer;
            transition: all 0.3s ease;
        }
        
        .bubble:hover {
            stroke: #333;
            stroke-width: 2px;
            filter: brightness(1.1);
        }
        
        .bubble-label {
            pointer-events: none;
            font-size: 12px;
            text-anchor: middle;
            fill: white;
            font-weight: bold;
            text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.5);
        }
        
        .tooltip {
            position: absolute;
            background-color: rgba(255, 255, 255, 0.95);
            padding: 12px 15px;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.3s ease, transform 0.3s ease;
            z-index: 100;
            transform: translateY(10px);
            border-left: 4px solid #228B22;
            min-width: 150px;
        }
        
        .tooltip.visible {
            opacity: 1;
            transform: translateY(0);
        }
        
        .tooltip-title {
            font-weight: bold;
            margin-bottom: 5px;
            font-size: 14px;
        }
        
        .tooltip-value {
            color: #228B22;
            font-weight: bold;
            font-size: 16px;
        }
        
        /* 滚动条美化 */
        ::-webkit-scrollbar {
            width: 8px;
        }
        
        ::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 4px;
        }
        
        ::-webkit-scrollbar-thumb {
            background: #32CD32;
            border-radius: 4px;
        }
        
        ::-webkit-scrollbar-thumb:hover {
            background: #228B22;
        }
        
        /* 加载动画 */
        .loading {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100%;
        }
        
        .loading-spinner {
            width: 50px;
            height: 50px;
            border: 5px solid #f3f3f3;
            border-top: 5px solid #32CD32;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        /* 页脚样式 */
        footer {
            background-color: #333;
            color: white;
            padding: 20px 0;
            text-align: center;
            margin-top: 50px;
        }
    </style>
</head>
<body>
    <header>
        <div class="container">
            <h1>豆瓣书籍数据可视化</h1>
            <p>基于豆瓣热门书籍数据的交互式可视化展示</p>
        </div>
    </header>
    
    <div class="container">
        <!-- 书籍列表部分 -->
        <h2>热门书籍列表</h2>
        <div class="books-scroll" id="books-container">
            <div class="loading">
                <div class="loading-spinner"></div>
            </div>
        </div>
        
        <!-- 分类气泡图 -->
        <h2>书籍分类分布</h2>
        <div class="chart-container">
            <div class="chart-title">气泡大小表示该分类的书籍数量，悬停可查看详细信息</div>
            <div id="category-tooltip" class="tooltip">
                <div class="tooltip-title" id="category-name"></div>
                <div class="tooltip-value" id="category-value"></div>
            </div>
            <svg id="category-chart"></svg>
        </div>
        
        <!-- 作者气泡图 -->
        <h2>作者作品分布</h2>
        <div class="chart-container">
            <div class="chart-title">气泡大小表示该作者的作品数量，悬停可查看详细信息</div>
            <div id="author-tooltip" class="tooltip">
                <div class="tooltip-title" id="author-name"></div>
                <div class="tooltip-value" id="author-value"></div>
            </div>
            <svg id="author-chart"></svg>
        </div>
    </div>
    
    <footer>
        <div class="container">
            <p>豆瓣书籍数据可视化 &copy; 2024</p>
        </div>
    </footer>
    
    <script>
        // 初始化数据
        let booksData = [];
        let categoryData = [];
        let authorData = [];
        
        // 加载数据
        async function loadData() {
            try {
                // 并行加载所有数据
                const [booksResponse, categoryResponse, authorResponse] = await Promise.all([
                    fetch('./data/douban_books.json'),
                    fetch('./data/category_stats.json'),
                    fetch('./data/author_stats.json')
                ]);
                
                booksData = await booksResponse.json();
                categoryData = await categoryResponse.json();
                authorData = await authorResponse.json();
                
                // 初始化可视化
                renderBooksList();
                createCategoryBubbleChart();
                createAuthorBubbleChart();
                
            } catch (error) {
                console.error('数据加载失败:', error);
                document.getElementById('books-container').innerHTML = `
                    <div style="text-align: center; padding: 50px 20px;">
                        <h3 style="color: #d32f2f; margin-bottom: 10px;">数据加载失败</h3>
                        <p>无法加载书籍数据，请检查数据文件是否存在。</p>
                    </div>
                `;
            }
        }
        
        // 渲染书籍列表
        function renderBooksList() {
            const booksContainer = document.getElementById('books-container');
            
            // 清空加载动画
            booksContainer.innerHTML = '';
            
            // 创建书籍列表
            const booksGrid = document.createElement('div');
            booksGrid.className = 'books-container';
            
            // 按评分排序
            const sortedBooks = [...booksData].sort((a, b) => (b.rating || 0) - (a.rating || 0));
            
            // 渲染每本书
            sortedBooks.forEach(book => {
                const bookCard = document.createElement('div');
                bookCard.className = 'book-card';
                
                // 生成星级评分
                const rating = book.rating || 0;
                const fullStars = Math.floor(rating / 2);
                const hasHalfStar = (rating % 2) >= 0.5;
                let starsHTML = '';
                
                for (let i = 0; i < fullStars; i++) {
                    starsHTML += '★';
                }
                
                if (hasHalfStar) {
                    starsHTML += '☆';
                }
                
                for (let i = 0; i < (5 - fullStars - (hasHalfStar ? 1 : 0)); i++) {
                    starsHTML += '☆';
                }
                
                // 处理分类标签
                let categoriesHTML = '';
                if (Array.isArray(book.categories) && book.categories.length > 0) {
                    categoriesHTML = book.categories.slice(0, 3).map(category => 
                        `<span class="category-tag">${category}</span>`
                    ).join('');
                } else {
                    categoriesHTML = '<span class="category-tag">未分类</span>';
                }
                
                // 构建书籍卡片HTML
                bookCard.innerHTML = `
                    <div class="book-cover">
                        <img src="${getValidImagePath(book)}" 
                             alt="${book.title || '未知书名'}" onerror="this.src='https://img3.doubanio.com/view/subject/s/public/s1070959.jpg'">
                    </div>
                    <div class="book-info">
                        <div class="book-title">${book.title || '未知书名'}</div>
                        <div class="book-author">${book.author || '未知作者'}</div>
                        <div class="book-rating">
                            <div class="rating-stars">${starsHTML}</div>
                            <div class="rating-value">${book.rating || 'N/A'}</div>
                        </div>
                        <div class="book-categories">
                            ${categoriesHTML}
                        </div>
                    </div>
                `;
                
                booksGrid.appendChild(bookCard);
            });
            
            booksContainer.appendChild(booksGrid);
        }
        
        // 创建分类气泡图
        function createCategoryBubbleChart() {
            // 检查数据
            if (!categoryData || categoryData.length === 0) {
                document.getElementById('category-chart').innerHTML = `
                    <text x="50%" y="50%" text-anchor="middle" fill="#999">
                        暂无分类数据
                    </text>
                `;
                return;
            }
            
            const svg = d3.select('#category-chart');
            const width = svg.node().getBoundingClientRect().width;
            const height = 400;
            const tooltip = d3.select('#category-tooltip');
            
            // 清空SVG
            svg.selectAll("*").remove();
            
            // 创建比例尺
            const maxValue = d3.max(categoryData, d => d.value);
            const minValue = d3.min(categoryData, d => d.value);
            
            // 调整气泡大小范围，使小类别更明显
            const radiusScale = d3.scaleSqrt()
                .domain([minValue, maxValue])
                .range([20, 70]);
                
            const colorScale = d3.scaleOrdinal()
                .domain(categoryData.map(d => d.name))
                .range(d3.schemeSet3);
            
            // 创建模拟力场 - 减小间距，让气泡更紧凑
            const simulation = d3.forceSimulation(categoryData)
                .force('charge', d3.forceManyBody().strength(0))
                .force('center', d3.forceCenter(width / 2, height / 2))
                .force('collision', d3.forceCollide().radius(d => radiusScale(d.value) + 1))
                .force('x', d3.forceX(width / 2).strength(0.1))
                .force('y', d3.forceY(height / 2).strength(0.1))
                .alphaDecay(0.02) // 减缓衰减速度，让气泡有更多时间找到位置
                .on('tick', ticked);
            
            // 创建包含气泡和标签的组
            const bubbleGroups = svg.selectAll('.bubble-group')
                .data(categoryData)
                .enter()
                .append('g')
                .attr('class', 'bubble-group')
                .on('mouseover', handleMouseOver)
                .on('mousemove', handleMouseMove)
                .on('mouseout', handleMouseOut);
            
            // 创建气泡
            bubbleGroups.append('circle')
                .attr('class', 'bubble')
                .attr('r', d => radiusScale(d.value))
                .attr('fill', d => colorScale(d.name))
                .attr('fill-opacity', 0.8)
                .attr('stroke', d => d3.color(colorScale(d.name)).darker(0.5))
                .attr('stroke-width', 1.5)
                .attr('stroke-opacity', 0.6);
            
            // 创建标签
            bubbleGroups.append('text')
                .attr('class', 'bubble-label')
                .text(d => {
                    if (d.name.length > 6) {
                        return d.name.substring(0, 6) + '...';
                    }
                    return d.name;
                })
                .style('font-size', d => {
                    // 根据气泡大小调整字体大小
                    const size = radiusScale(d.value) / 3;
                    return Math.max(10, Math.min(size, 16)) + 'px';
                });
            
            // 鼠标悬停事件处理
            function handleMouseOver(event, d) {
                const bubble = d3.select(this).select('circle');
                
                bubble
                    .attr('fill-opacity', 1)
                    .attr('stroke', '#333')
                    .attr('stroke-width', 2)
                    .attr('stroke-opacity', 1);
                
                tooltip.classed('visible', true)
                    .style('opacity', 1)
                    .style('left', (event.pageX + 15) + 'px')
                    .style('top', (event.pageY - 40) + 'px')
                    .style('border-color', colorScale(d.name));
                
                d3.select('#category-name').text(d.name);
                d3.select('#category-value').text(`${d.value} 本书籍`);
            }
            
            function handleMouseMove(event) {
                tooltip
                    .style('left', (event.pageX + 15) + 'px')
                    .style('top', (event.pageY - 40) + 'px');
            }
            
            function handleMouseOut() {
                const bubble = d3.select(this).select('circle');
                
                bubble
                    .attr('fill-opacity', 0.8)
                    .attr('stroke', d => d3.color(colorScale(d.name)).darker(0.5))
                    .attr('stroke-width', 1.5)
                    .attr('stroke-opacity', 0.6);
                
                tooltip.classed('visible', false)
                    .style('opacity', 0);
            }
            
            // 更新气泡位置
            function ticked() {
                bubbleGroups
                    .attr('transform', d => {
                        const x = Math.max(radiusScale(d.value), Math.min(width - radiusScale(d.value), d.x));
                        const y = Math.max(radiusScale(d.value), Math.min(height - radiusScale(d.value), d.y));
                        return `translate(${x}, ${y})`;
                    });
            }
        }
        
        // 创建作者气泡图
        function createAuthorBubbleChart() {
            // 检查数据
            if (!authorData || authorData.length === 0) {
                document.getElementById('author-chart').innerHTML = `
                    <text x="50%" y="50%" text-anchor="middle" fill="#999">
                        暂无作者数据
                    </text>
                `;
                return;
            }
            
            const svg = d3.select('#author-chart');
            const width = svg.node().getBoundingClientRect().width;
            const height = 400;
            const tooltip = d3.select('#author-tooltip');
            
            // 清空SVG
            svg.selectAll("*").remove();
            
            // 显示前40位作者，增加更多节点密度
            const topAuthors = authorData.slice(0, 40);
            
            // 创建比例尺
            const maxValue = d3.max(topAuthors, d => d.value);
            const minValue = d3.min(topAuthors, d => d.value);
            
            // 调整气泡大小比例
            const radiusScale = d3.scaleSqrt()
                .domain([minValue, maxValue])
                .range([15, 45]);
                
            // 使用更丰富的颜色方案
            const colorScale = d3.scaleOrdinal()
                .domain(topAuthors.map(d => d.name))
                .range([...d3.schemeSet3, ...d3.schemeCategory10, ...d3.schemePastel1]);
            
            // 创建模拟力场 - 改进布局
            const simulation = d3.forceSimulation(topAuthors)
                .force('charge', d3.forceManyBody().strength(5))
                .force('center', d3.forceCenter(width / 2, height / 2))
                .force('collision', d3.forceCollide().radius(d => radiusScale(d.value) + 2))
                .force('x', d3.forceX(width / 2).strength(0.07))
                .force('y', d3.forceY(height / 2).strength(0.07))
                .alphaDecay(0.02)
                .on('tick', ticked);
            
            // 创建包含气泡和标签的组
            const bubbleGroups = svg.selectAll('.author-bubble-group')
                .data(topAuthors)
                .enter()
                .append('g')
                .attr('class', 'bubble-group')
                .on('mouseover', handleMouseOver)
                .on('mousemove', handleMouseMove)
                .on('mouseout', handleMouseOut);
            
            // 创建气泡
            bubbleGroups.append('circle')
                .attr('class', 'bubble')
                .attr('r', d => radiusScale(d.value))
                .attr('fill', d => colorScale(d.name))
                .attr('fill-opacity', 0.8)
                .attr('stroke', d => d3.color(colorScale(d.name)).darker(0.5))
                .attr('stroke-width', 1)
                .attr('stroke-opacity', 0.6);
            
            // 创建标签
            bubbleGroups.append('text')
                .attr('class', 'bubble-label')
                .text(d => {
                    if (d.name.length > 4) {
                        return d.name.substring(0, 4) + '...';
                    }
                    return d.name;
                })
                .style('font-size', d => {
                    // 根据气泡大小调整字体大小
                    const size = radiusScale(d.value) / 2.5;
                    return Math.max(9, Math.min(size, 14)) + 'px';
                });
            
            // 鼠标悬停事件处理
            function handleMouseOver(event, d) {
                const bubble = d3.select(this).select('circle');
                
                bubble
                    .attr('fill-opacity', 1)
                    .attr('stroke', '#333')
                    .attr('stroke-width', 2)
                    .attr('stroke-opacity', 1);
                
                tooltip.classed('visible', true)
                    .style('opacity', 1)
                    .style('left', (event.pageX + 15) + 'px')
                    .style('top', (event.pageY - 40) + 'px')
                    .style('border-color', colorScale(d.name));
                
                d3.select('#author-name').text(d.name);
                d3.select('#author-value').text(`${d.value} 本作品`);
            }
            
            function handleMouseMove(event) {
                tooltip
                    .style('left', (event.pageX + 15) + 'px')
                    .style('top', (event.pageY - 40) + 'px');
            }
            
            function handleMouseOut() {
                const bubble = d3.select(this).select('circle');
                
                bubble
                    .attr('fill-opacity', 0.8)
                    .attr('stroke', d => d3.color(colorScale(d.name)).darker(0.5))
                    .attr('stroke-width', 1)
                    .attr('stroke-opacity', 0.6);
                
                tooltip.classed('visible', false)
                    .style('opacity', 0);
            }
            
            // 更新气泡位置
            function ticked() {
                bubbleGroups
                    .attr('transform', d => {
                        const x = Math.max(radiusScale(d.value), Math.min(width - radiusScale(d.value), d.x));
                        const y = Math.max(radiusScale(d.value), Math.min(height - radiusScale(d.value), d.y));
                        return `translate(${x}, ${y})`;
                    });
            }
        }
        
        // 页面加载完成后初始化
        document.addEventListener('DOMContentLoaded', loadData);

        // 监听窗口大小变化，重新绘制图表
        window.addEventListener('resize', () => {
            createCategoryBubbleChart();
            createAuthorBubbleChart();
        });

        // 辅助函数：获取有效的图片路径
        function getValidImagePath(book) {
            // 检查是否有本地图片路径
            if (book.local_img_path && book.local_img_path !== 'null' && book.local_img_path !== 'undefined') {
                // 处理可能的路径编码问题
                try {
                    return decodeURIComponent(book.local_img_path);
                } catch (e) {
                    console.warn('图片路径解码失败:', book.local_img_path, e);
                    return book.local_img_path;
                }
            }
            
            // 如果本地路径无效，回退到在线URL
            if (book.img_url && book.img_url !== 'null' && book.img_url !== 'undefined') {
                return book.img_url;
            }
            
            // 最后的回退选项：默认图片
            return 'https://img3.doubanio.com/view/subject/s/public/s1070959.jpg';
        }
    </script>
</body>
</html> 